<?php

	namespace org\tekuna\core\configuration;


	/**
	 * The ConfigurationElement is the central node type of an
	 * application configuration tree. It supports hierarchic composition
	 * of ConfigurationElements and the assignments of multiple
	 * ConfigurationAttributes.
	 */
	class ConfigurationElement extends ConfigurationNode {

		private

			/** the parent ConfigurationElement or NULL if there is none */
			$objParentElement = NULL,

			/** the list of child elements of type ConfigurationElement */
			$arrChildElements = array(),

			/** the list of attributes of type ConfigurationAttribute */
			$arrAttributes = array();


		/**
		 * Set a new parent element. If there is already another parent element
		 * this element removes itself from this (old) parent element and assigns
		 * the given new parent.
		 *
		 * @param ConfigurationElement $objParentElement the new parent element
		 * @param boolean $bDisconnectParent calls removeChildElement on the old parent if set to true
		 * @param boolean $bPopulateChild calls addChildElement on the new parent if set to true
		 */
		public function setParentElement(ConfigurationElement $objParentElement, $bDisconnectParent = true, $bPopulateChild = true) {

			// disconnect from old parent if present
			if ($bDisconnectParent && $this -> objParentElement !== NULL) {

				$this -> objParentElement -> removeChildElement($this, false);
			}

			// add new child on parent
			if ($bPopulateChild && $objParentElement !== NULL) {

				$objParentElement -> addChildElement($this, false);
			}

			// set the new parent
			$this -> objParentElement = $objParentElement;
		}


		/**
		 * Returns the parent element of this ConfigurationElement.
		 *
		 * @return ConfigurationElement the parent element or NULL if there is none
		 */
		public function getParentElement() {

			return $this -> objParentElement;
		}


		/**
		 * Add a new child element to this element. This method does NOT check
		 * if there is still another duplicate child element.
		 *
		 * @param ConfigurationElement $objChildElement the new child element
		 * @param boolean $bUpdateParent calls setParentElement on the new child if set to true
		 * @throws ElementException if the given child element's parent is already
		 *                                this element (try to add a new child that is
		 *                                still child of this object
		 */
		public function addChildElement(ConfigurationElement $objChildElement, $bUpdateParent = true) {

			// update parent if necessary
			if ($objChildElement -> getParentElement() === $this) {

				throw new ElementException("Cannot add a child element that is already a child element of this ConfigurationElement.");
			}
			else {

				// update the parent
				if ($bUpdateParent) {

					$objChildElement -> setParentElement($this, true, false);
				}

				// store the new children
				$this -> arrChildElements[] = $objChildElement;
			}
		}


		public function insertChildElementAfter(ConfigurationElement $objChildElement, ConfigurationElement $objPreviouseChildElement, $bUpdateParent = true) {

			// update parent if necessary
			if ($objChildElement -> getParentElement() === $this) {

				throw new ElementException("Cannot add a child element that is already a child element of this ConfigurationElement.");
			}
			else {

				// update the parent
				if ($bUpdateParent) {

					$objChildElement -> setParentElement($this, true, false);
				}

				// store the new children
				$arrNewChildren = array();
				foreach ($this -> arrChildElements as $objExistingChildElement) {
					
					$arrNewChildren[] = $objExistingChildElement;
					
					if ($objExistingChildElement === $objPreviouseChildElement) {
						
						$arrNewChildren[] = $objChildElement;
					}
				}
				
				$this -> arrChildElements = $arrNewChildren;
			}
		}
		
		
		/**
		 * Removes the specified child element from the list of child elements. If
		 * there is no corresponding element, a ConfigurationException is thrown.
		 *
		 * @param ConfigurationElement $objChildElement the child element to remove
		 * @param boolean $bUpdateParent calls setParentElement(null) on the removed children if set to true
		 * @throws NoSuchElementException if there is no matching child element
		 */
		public function removeChildElement(ConfigurationElement $objChildElement, $bUpdateParent = true) {

			foreach ($this -> getChildElements() as $i => $objAssignedChildElement) {

				if ($objAssignedChildElement === $objChildElement) {

					unset($this -> arrChildElements[$i]);

					if ($bUpdateParent) {

						$objChildElement -> setParentElement(null, false, true);
					}

					return;
				}
			}

			throw new NoSuchElementException("The specified ConfigurationElement is no child of this ConfigurationElement.");
		}


		/**
		 * Returns the array of all child elements.
		 *
		 * @return array of child elements of type ConfigurationElement
		 */
		public function getChildElements() {

			return $this -> arrChildElements;
		}


		/**
		 * Checks if there is a child element for the given aspect and name.
		 *
		 * @param string $sAspect the aspect to look for
		 * @param string $sName the name to look for
		 * @return boolean returns true if there is at least one child element with the given aspect and name.
		 */
		public function hasChildElement($sAspect, $sName) {

			foreach ($this -> arrChildElements as $objChildElement) {

				if ($objChildElement -> getAspect() == $sAspect
				    && $objChildElement -> getName() == $sName) {

					return true;
				}
			}

			return false;
		}


		/**
		 * This method returns the first child element that matches the given aspect and name. If there is
		 * no matching child element, a ConfigurationExeption is thrown.
		 *
		 * @param string $sAspect the aspect to look for
		 * @param string $sName the name to look for
		 * @throws ConfigurationException if there is no matching child element
		 * @return NoSuchElementException the first matched child element
		 */
		public function getFirstChildElement($sAspect, $sName) {

			foreach ($this -> arrChildElements as $objChildElement) {

				if ($objChildElement -> getAspect() == $sAspect
				    && $objChildElement -> getName() == $sName) {

					return $objChildElement;
				}
			}

			throw new NoSuchElementException("This ConfigurationElement does not have a child element $sAspect:$sName.");
		}


		/**
		 * Returns all matching child elements that have the given aspect and name.
		 * Returns an empty array if there are no matching child elements.
		 *
		 * @param string $sAspect the aspect to look for
		 * @param string $sName the name to look for
		 * @return array of matched child elemtns of type ConfigurationElement^
		 */
		public function getAllChildElements($sAspect, $sName) {

			$arrElements = array();

			foreach ($this -> arrChildElements as $objChildElement) {

				if ($objChildElement -> getAspect() == $sAspect
				    && $objChildElement -> getName() == $sName) {

					$arrElements[] = $objChildElement;
				}
			}

			return $arrElements;
		}


		/**
		 * Adds a new attribute node to this element. This method does NOT check
		 * if there is still another attribute with the same aspect and name.
		 *
		 * @param ConfigurationAttribute $objAttribute the new attribute.
		 * @param boolean $bUpdateParent calls setElement on the new attribute if set to true
		 * @throws AttributeException if the attribute is already assigned to this element
		 */
		public function addAttribute(ConfigurationAttribute $objAttribute, $bUpdateParent = true) {

			// update parent if necessary
			if ($objAttribute -> getElement() === $this) {

				throw new AttributeException("Cannot add an attribute that is already assigned to this element.");
			}
			else {

				// update the parent
				if ($bUpdateParent) {

					$objAttribute -> setElement($this, true, false);
				}

				// store the new attribute
				$this -> arrAttributes[] = $objAttribute;
			}
		}


		/**
		 * This method removes the given attribute from this element. If there is
		 * no such attribute, a ConfigurationException is thrown.
		 *
		 * @param ConfigurationAttribute $objAttribute the attribute to remove
		 * @param boolean $bUpdateParent calls setElement(null) on the removed attribute if set to true
		 * @throws NoSuchAttributeException if the given attribute is no child of this element
		 */
		public function removeAttribute(ConfigurationAttribute $objAttribute, $bUpdateParent = true) {

			foreach ($this -> getAttributes() as $i => $objAssignedAttribute) {

				if ($objAssignedAttribute === $objAttribute) {

					unset($this -> arrAttributes[$i]);

					if ($bUpdateParent) {

						$objAttribute -> setElement(null, false, true);
					}

					return;
				}
			}

			throw new NoSuchAttributeException("The specified ConfigurationAttribute is no child of this ConfigurationElement.");
		}


		/**
		 * Returns the array of all attribute objects of this element.
		 *
		 * @return array of ConfigurationAttributes
		 */
		public function getAttributes() {

			return $this -> arrAttributes;
		}


		/**
		 * Checks if there is at least one attribute with the given aspect and name.
		 *
		 * @param string $sAspect the aspect to look for
		 * @param string $sName the name to look for
		 * @return boolean returns true if there is at least one attribute with the given aspect and name
		 */
		public function hasAttribute($sAspect, $sName) {

			foreach ($this -> arrAttributes as $objAttribute) {

				if ($objAttribute -> getAspect() == $sAspect
				    && $objAttribute -> getName() == $sName) {

					return true;
				}
			}

			return false;
		}


		/**
		 * Returns the (first) attribute with the given aspect and name. Due to common semantics
		 * there should be only one aspect with a certain aspect and name on every element. But this
		 * semantics is not enforced by this class. Throws a ConfigurationException if there is no
		 * such attribute
		 *
		 * @param string $sAspect the aspect of the attribute
		 * @param string $sName the name of the attribute
		 * @throws NoSuchAttributeException if there is no attribute with the given aspect and name
		 */
		public function getAttribute($sAspect, $sName) {

			foreach ($this -> arrAttributes as $objAttribute) {

				if ($objAttribute -> getAspect() == $sAspect
				    && $objAttribute -> getName() == $sName) {

					return $objAttribute;
				}
			}

			throw new NoSuchAttributeException("This ConfigurationElement does not have an attribute $sAspect:$sName.\n". $this -> __toString());
		}


		/**
		 * This method replaces the given old aspect with the new aspect on this ConfigurationElement, all of
		 * its ConfigurationAttributes and all its child ConfigurationElements. This method does NOT recurse into
		 * the parent element. So if you wish to change the aspect on the whole Configuration tree you have to
		 * call this method on the root element.
		 *
		 * @param string $sOldAspect the old aspect
		 * @param string $sNewAspect the new aspect
		 */
		public function changeAspect($sOldAspect, $sNewAspect) {

			// change this objects aspect
			if ($this -> getAspect() == $sOldAspect) {

				$this -> setAspect($sNewAspect);
			}

			// change aspects of attributes
			foreach ($this -> arrAttributes as $objAttribute) {

				if ($objAttribute -> getAspect() == $sOldAspect) {

					$objAttribute -> setAspect($sNewAspect);
				}
			}

			// change aspects of child elements
			foreach ($this -> arrChildElements as $objChildElement) {

				if ($objChildElement -> getAspect() == $sOldAspect) {

					$objChildElement -> setAspect($sNewAspect);
				}

				// recurse into child elements
				$objChildElement -> changeAspect($sOldAspect, $sNewAspect);
			}
		}


		/**
		 * Returns a nicely formatted string representation of this ConfigurationElement,
		 * all the child ConfigurationElements and all ConfigurationAttributes. This method
		 * works recursively and uses the __toString() method of the ConfigurationAttribute.
		 *
		 * @returns string representation of this ConfigurationElement, all its children and attributes.
		 */
		public function __toString() {

			// determine depth of element
			$iDepth = -1;
			$objCurrentPointer = $this;
			do {

				$objCurrentPointer = $objCurrentPointer -> getParentElement();
				$iDepth++;
			}
			while ($objCurrentPointer !== NULL);

			// convert element, attributes and child elements
			$sOut = str_repeat('   ', $iDepth) . "$this->sAspect:$this->sName [";
			$sOut .= implode(', ', $this -> getAttributes());
			$sOut .= "] {" . $this -> sValue ."}\n";
			$sOut .= implode("\n", $this -> getChildElements());

			return $sOut;
		}
	}
